/*
    hdvb - Framework for sophisticated DVB applications
    Copyright (C) 2012 Tony Houghton <h@realh.co.uk>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* Tunes to a chosen multiplex, optionally allowing a single service to
 * be recorded.
 */

#include <glib.h>

#include <gst/gst.h>

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <hdvb/cacheddb.h>
#include <hdvb/chansdb.h>
#include <hdvb/eitdb.h>
#include <hdvb/hdvb-eit-freesat.h>
#include <hdvb/hdvb-eit-harvest.h>
#include <hdvb/hdvb-receiver.h>
#include <hdvb/hdvb-lookup-service-pids.h>

#include <hdvb/gst/hdvb-gst-stream-src.h>

typedef struct {
    int adapter;
    int frontend;
    char *db_filename;
    char *tuning_str;
    int lcn;
    int service_id;
    char *service_name;
    char *recording_name;
    char *eit_name;
    gboolean freesat;
    gboolean freeviewhd;
} Options;

typedef struct {
    const Options *options;
    HdvbReceiver *rcv;
    sqlite3 *database;
    GstElement *pipeline, *parser, *sink;
    GstElement *ssrc;
} Context;

#define SPACE "\t\t\t\t    "

Options *
get_options(int *argc, char ***argv)
{
    GError *error = NULL;
    static Options options = { 0, 0, NULL, NULL, -1, -1, NULL, NULL,
            NULL, FALSE };
    static GOptionEntry entries[] = {
        { "adapter", 'a', 0, G_OPTION_ARG_INT, &options.adapter,
                "Adapter number A.", "A" },
        { "frontend", 'f', 0, G_OPTION_ARG_INT, &options.frontend,
                "Front-end number F.", "F" },
        { "database", 'd', 0, G_OPTION_ARG_FILENAME, &options.db_filename,
                "Lookup service in transport sqlite database\n"
                SPACE "DATABASE. Optional if --tuning/-t is used.",
                "DATABASE" },
        { "tuning", 't', 0, G_OPTION_ARG_STRING, &options.tuning_str,
                "Tune using TUNING string, select no service.", "TUNING" },
        { "lcn", 'l', 0, G_OPTION_ARG_INT, &options.lcn,
                "Select channel by lcn.", "L" },
        { "service-id", 's', 0, G_OPTION_ARG_INT, &options.service_id,
                "Select channel by service id.", "S" },
        { "service-name", 'n', 0, G_OPTION_ARG_STRING, &options.service_name,
                "Select channel by service name.", "NAME" },
        { "record", 'r', 0, G_OPTION_ARG_FILENAME, &options.recording_name,
                "Record service to FILENAME; not compatible\n"
                SPACE "with --tuning.",
                "FILENAME" },
        { "eit", 'e', 0, G_OPTION_ARG_FILENAME, &options.eit_name,
                "Harvest EIT data into sqlite file EITDBASE.",
                "EITDBASE" },
        { "freesat", 'p', 0, G_OPTION_ARG_NONE, &options.freesat,
                "Harvest Freesat's eit.", NULL },
        { "freeviewhd", 'h', 0, G_OPTION_ARG_NONE, &options.freeviewhd,
                "EIT contains Freeview HD encoding.", NULL },
        { NULL },
    };
    GOptionContext *octx = g_option_context_new(
            " - tune and optionally record from a DVB device");
    int nsel = 0;

    g_option_context_add_main_entries(octx, entries, "hdvb-tune");
    g_option_context_add_group(octx, gst_init_get_option_group());
    if (!g_option_context_parse(octx, argc, argv, &error))
    {
        fprintf(stderr, "Option parsing failed: %s\n", error->message);
        exit(1);
    }
    if (*argc != 1)
    {
        fprintf(stderr, "Usage error: Surplus arguments.\n");
        exit(1);
    }
    if (options.lcn > 0)
        ++nsel;
    if (options.service_id > 0)
        ++nsel;
    if (options.service_name)
        ++nsel;
    if (options.tuning_str)
        ++nsel;
    if (nsel != 1)
    {
        fprintf(stderr, "Usage error: "
                "Must specify exactly one way to tune/select service.\n");
        exit(1);
    }
    if (!options.tuning_str && !options.db_filename)
    {
        fprintf(stderr, "Usage error: DATABASE not specified.\n");
        exit(1);
    }
    else if (options.tuning_str && options.db_filename)
    {
        fprintf(stderr, "Warning: DATABASE not required with TUNING.\n");
        g_free(options.db_filename);
        options.db_filename = NULL;
    }
    return &options;
}

/* Also looks up service_id if lcn or name are given */
sqlite3 *
open_database(Options *options)
{
    sqlite3 *database = hdvb_chans_db_open(options->db_filename, NULL, FALSE);

    if (!database)
        exit(1);
    if (options->lcn > -1)
    {
        sqlite3_stmt *stmt = hdvb_db_prepare_statement(database,
                "SELECT service_id FROM services WHERE lcn = ?");

        if (!stmt)
            exit(1);
        sqlite3_bind_int(stmt, 1, options->lcn);
        if (hdvb_db_step(stmt) != SQLITE_ROW)
        {
            fprintf(stderr, "Can't find channel with lcn %d\n", options->lcn);
            exit(1);
        }
        options->service_id = sqlite3_column_int(stmt, 0);
        sqlite3_finalize(stmt);
    }
    else if (options->service_name)
    {
        sqlite3_stmt *stmt = hdvb_db_prepare_statement(database,
                "SELECT service_id FROM services WHERE service_name = ?");

        if (!stmt)
            exit(1);
        sqlite3_bind_text(stmt, 1, options->service_name, -1, SQLITE_TRANSIENT);
        if (hdvb_db_step(stmt) != SQLITE_ROW)
        {
            fprintf(stderr, "Can't find channel named '%s'\n",
                    options->service_name);
            exit(1);
        }
        options->service_id = sqlite3_column_int(stmt, 0);
        sqlite3_finalize(stmt);
    }
    printf("Tuning to service_id %d\n", options->service_id);
    return database;
}

static void
parser_pad_added_cb(Context *context, GstPad *src_pad, gpointer obj)
{
    char *name = gst_pad_get_name(src_pad);

    g_debug("Parser pad added: '%s'", name);
    g_free(name);

    GstPad *dest_pad = gst_element_get_static_pad(context->sink, "sink");

    gst_pad_link(src_pad, dest_pad);
    gst_object_unref(dest_pad);
}

static void start_recording(Context *context, const HdvbServicePidsInfo *pids)
{
    int result;

    g_return_if_fail(pids != NULL);
    printf("Recording service %d to '%s'\n",
            context->options->service_id, context->options->recording_name);
    context->pipeline = gst_pipeline_new("hdvb-pipeline");
    context->ssrc = GST_ELEMENT(hdvb_receiver_get_stream_src(context->rcv));
    gst_object_ref(GST_OBJECT(context->ssrc));
    context->parser = gst_element_factory_make("mpegtsparse", "parser");
    char progs[32];
    sprintf(progs, "%d", pids->service_id);
    g_object_set(context->parser, "program-numbers", progs, NULL);
    g_signal_connect_swapped(context->parser, "pad-added",
            G_CALLBACK(parser_pad_added_cb), context);
    context->sink = gst_element_factory_make("filesink", "sink");
    g_object_set(context->sink,
            "location", context->options->recording_name, NULL);
    gst_bin_add_many(GST_BIN(context->pipeline),
            context->ssrc, context->parser, context->sink, NULL);
    if (!gst_element_link(context->ssrc, context->parser))
    {
        fprintf(stderr, "Unable to link source to parser\n");
        exit(1);
    }
    result = hdvb_service_pids_info_request_filters(pids, context->rcv);
    if (result)
    {
        fprintf(stderr, "Error requesting pid filters: %s\n", strerror(errno));
        exit(1);
    }
    if (gst_element_set_state(context->pipeline, GST_STATE_PLAYING) ==
            GST_STATE_CHANGE_FAILURE)
    {
        fprintf(stderr, "Failed to start gstreamer pipeline\n");
        exit(1);
    }
}

static HdvbCachedDb *eit_db = NULL;
static void (*orig_signal_handler)(int);
static HdvbEitHarvest *harvester;

static gboolean
wait_for_pending_insertions(gpointer handle)
{
    if (!harvester->stopped)
    {
        printf("\nStopping EIT harvest\n");
        hdvb_eit_harvest_stop(harvester);
        printf("Waiting for queued EIT database insertions");
    }
    if (hdvb_eit_harvest_get_n_callbacks_pending(harvester))
    {
        putchar('.');
        return TRUE;
    }
    printf("\nSaving EIT database to %s\n", eit_db->filename);
    hdvb_cached_db_sync(eit_db);
    exit(0);
    return FALSE;
}

static void
stop_harvesting_eit(int signum)
{
    signal(signum, orig_signal_handler);
    g_idle_add(wait_for_pending_insertions, NULL);
}

static void
start_harvesting_eit(Context *context)
{
    const char *enc;

    if (context->options->freesat)
        enc = "freesat";
    else if (context->options->freeviewhd)
        enc = "freeviewhd";
    else
        enc = "dvb";
    printf("Harvesting EIT data...\n");
    eit_db = hdvb_cached_db_open(context->options->eit_name,
            TRUE, FALSE,
            hdvb_eit_db_create_schemas, HDVB_EIT_DB_VERSION, (gpointer) enc);
    g_return_if_fail(eit_db != NULL);
    orig_signal_handler = signal(SIGINT, stop_harvesting_eit);
    harvester = context->options->freesat ?
            hdvb_eit_freesat_new() : hdvb_eit_harvest_new();
    hdvb_eit_harvest_start(harvester, context->rcv, eit_db->db);
}

static void
wait_indefinitely(Context *context, const HdvbServicePidsInfo *pids)
{
    if (context->options->eit_name)
        start_harvesting_eit(context);
    if (context->options->recording_name)
        start_recording(context, pids);
    printf("Press Ctrl-C to exit\n");
}

static void
lock_cb(HdvbReceiver *rcv, gboolean locked, Context *context)
{
    if (!locked)
    {
        fprintf(stderr, "Signal lock failed\n");
        exit(1);
    }
    printf("Locked to signal\n");
    wait_indefinitely(context, NULL);
}

static void
pids_cb(HdvbReceiver *rcv, const HdvbServicePidsInfo *pids, Context *context)
{
    if (!pids)
    {
        fprintf(stderr, "Failed to find service in stream\n");
        exit(1);
    }
    printf("Found service details\n");
    wait_indefinitely(context, pids);
}

int
main(int argc, char **argv)
{
    GMainLoop *main_loop;
    Options *options;
    static Context context;

    memset(&context, 0, sizeof(Context));
    context.options = options = get_options(&argc, &argv);

    if (options->db_filename)
    {
        if (!g_file_test(options->db_filename, G_FILE_TEST_EXISTS))
        {
            fprintf(stderr, "Database %s not found", options->db_filename);
            exit(1);
        }
        context.database = open_database(options);
    }
    else
    {
        context.database = NULL;
    }

    main_loop = g_main_loop_new(NULL, FALSE);

    hdvb_receiver_udev_init(NULL, NULL);
    context.rcv = hdvb_receiver_get_by_numbers(options->adapter,
            options->frontend);
    if (!context.rcv)
        exit(1);
    hdvb_receiver_set_stream_src_factory(context.rcv,
            HDVB_GST_STREAM_SRC_FACTORY,
            HDVB_GST_STREAM_SRC_DELETER,
            GINT_TO_POINTER(5000));
    if (options->tuning_str)
    {
        int result;
        HdvbTuningData td;

        if (!hdvb_tuning_string_to_data(options->tuning_str, &td))
        {
            fprintf(stderr, "Invalid tuning string '%s'\n",
                    options->tuning_str);
        }
        g_signal_connect(context.rcv, "lock", G_CALLBACK(lock_cb), &context);
        result = hdvb_receiver_tune(context.rcv, &td, options->tuning_str,
                5000);
        if (result)
        {
            fprintf(stderr, "Tuning failed: %s\n", strerror(result));
            exit(1);
        }
    }
    else
    {
        HdvbLookupServicePids *lookup = hdvb_lookup_service_pids_new();
        int result;

        g_signal_connect(lookup, "pids", G_CALLBACK(pids_cb), &context);
        result = hdvb_lookup_service_pids_start_with_db(lookup, context.rcv,
                context.database, 5000, options->service_id);
        if (result)
        {
            fprintf(stderr, "Tuning failed: %s\n", strerror(result));
            exit(1);
        }
    }

    g_main_loop_run(main_loop);
    return 0;
}
